/**
 ** This file is part of FK Klistret Extensions. FK Klistret Extensions 
 ** is free software: you can redistribute it and/or modify it under the 
 ** terms of the GNU General Public License as published by the Free 
 ** Software Foundation, either version 3 of the License, or (at your option) 
 ** any later version.

 ** FK Klistret Extensions is distributed in the hope that it will be 
 ** useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 ** General Public License for more details. You should have received a
 ** copy of the GNU General Public License along with FK Klistret Extensions. 
 ** If not, see <http://www.gnu.org/licenses/>
 */
package se.fk.klistret.migrate.service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import org.hibernate.StaleStateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import se.fk.klistret.migrate.exceptions.InfrastructureException;
import se.fk.klistret.migrate.exceptions.ApplicationException;
import se.fk.klistret.migrate.jaxb.MatrixContext;

import com.klistret.cmdb.ci.pojo.Element;
import com.klistret.cmdb.service.ElementService;
import com.klistret.cmdb.service.ElementTypeService;
import com.klistret.cmdb.service.IdentificationService;
import com.klistret.cmdb.service.RelationService;
import com.klistret.cmdb.service.RelationTypeService;

public abstract class BaseElementService {
	private static final Logger logger = LoggerFactory
			.getLogger(BaseElementService.class);

	/**
	 * Element service (dependency injection)
	 */
	protected ElementService elementService;

	/**
	 * Element type service (dependency injection)
	 */
	protected ElementTypeService elementTypeService;

	/**
	 * Relation service (dependency injection)
	 */
	protected RelationService relationService;

	/**
	 * Relation type service (dependency injection)
	 */
	protected RelationTypeService relationTypeService;

	/**
	 * Identification service
	 */
	protected IdentificationService identificationService;

	/**
	 * JAXB unmarshaller
	 */
	protected Unmarshaller unmarshaller;

	/**
	 * Set element service
	 * 
	 * @param elementService
	 */
	public void setElementService(ElementService elementService) {
		this.elementService = elementService;
	}

	/**
	 * Set element type service
	 * 
	 * @param elementTypeService
	 */
	public void setElementTypeService(ElementTypeService elementTypeService) {
		this.elementTypeService = elementTypeService;
	}

	/**
	 * Set relation service
	 * 
	 * @param relationService
	 */
	public void setRelationService(RelationService relationService) {
		this.relationService = relationService;
	}

	/**
	 * Set relation type service
	 * 
	 * @param relationTypeService
	 */
	public void setRelationTypeService(RelationTypeService relationTypeService) {
		this.relationTypeService = relationTypeService;
	}

	/**
	 * Set identification service
	 * 
	 * @param identificationService
	 */
	public void setIdentificationService(
			IdentificationService identificationService) {
		this.identificationService = identificationService;
	}

	protected abstract Element unmarshall(String line, String directory)
			throws JAXBException;

	/**
	 * Migrating without considering the origin identification
	 */
	public void migrate(String directory, String fileName, String encoding) {
		int lineCount = 0;
		int serviceCount = 0;
		int staleStateCount = 0;

		logger.info("Using directory [{}] and file name [{}]", directory,
				fileName);

		try {
			unmarshaller = MatrixContext.getMatrixContext().getJAXBContext()
					.createUnmarshaller();
		} catch (JAXBException e) {
			throw new InfrastructureException(String.format(
					"Unable create unmarshaller: %s", e.getMessage()));
		}

		try {
			BufferedReader br = new BufferedReader(new InputStreamReader(
					new FileInputStream(new File(directory, fileName)),
					encoding));

			String line = null;
			while ((line = br.readLine()) != null) {
				try {
					Element element = unmarshall(line, directory);

					/**
					 * If "origin" exists then update by replacing the
					 * marshalled element with the existing. No origin does a
					 * create if the identification rules pass.
					 */
					Element other = getOrigin(element);
					if (other != null) {
						other.setConfiguration(element.getConfiguration());

						logger.debug(
								"Updating an existing element [id: {}, name: {}] with origin [{}]",
								new Object[] {
										other.getId(),
										other.getName(),
										other.getConfiguration().getOrigin()
												.getIdentification() });
						elementService.update(other);
						serviceCount++;
					} else {
						if (identify(element))
							logger.warn(
									"Identification found similar element(s) to element [name: {}, type: {}]",
									element.getName(), element.getType()
											.getName());
						else {
							logger.debug(
									"Creating new element [id: {}, name: {}] with origin [{}]",
									new Object[] {
											element.getId(),
											element.getName(),
											element.getConfiguration()
													.getOrigin()
													.getIdentification() });
							elementService.create(element);

							serviceCount++;
						}

					}

				} catch (ApplicationException e) {
					logger.error(String.format(
							"Continue despite application error; %s",
							e.getMessage()));
				} catch (com.klistret.cmdb.exception.NonUniqueObjectException e) {
					if (e.getExpressions() != null) {
						logger.error("Unable perform unique call with the following expressions:");
						for (String expression : e.getExpressions())
							logger.error(expression);
					}
					throw e;
				} catch (com.klistret.cmdb.exception.ApplicationException e) {
					if (e.contains(StaleStateException.class)) {
						staleStateCount++;
						logger.error(e.getMessage());
					} else
						throw e;
				}

				lineCount++;
			}
		} catch (FileNotFoundException e) {
			logger.error("Unable to locate file: {}", e.getMessage());
		} catch (IOException e) {
			logger.error("Unable to handle file: {}", e.getMessage());
		} catch (JAXBException e) {
			logger.error("Unable to unmarshall Matrix to Klistret: {}",
					e.getMessage());
		} catch (Exception e) {
			logger.error("Unknown error: {}", e.getMessage());
		} finally {
			if (serviceCount != lineCount)
				logger.warn(
						"Difference between line count [{}] and service count [{}] with file [{}]",
						new Object[] { lineCount, serviceCount, fileName });

			logger.info("Completed {} lines from file [{}], stales [{}]",
					new Object[] { lineCount, fileName, staleStateCount });
		}
	}

	/**
	 * 
	 * @param element
	 * @return
	 */
	private Element getOrigin(Element element) {
		if (element.getConfiguration().getOrigin() == null
				|| element.getConfiguration().getOrigin().getIdentification() == null)
			return null;

		Element other = elementService
				.unique(Arrays.asList(new String[] { String
						.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; /pojo:Element[pojo:type/pojo:name eq \"%s\"]/pojo:configuration/commons:Origin[@Name eq \"Matrix\" and commons:Identification eq \"%s\"]",
								element.getType().getName(), element
										.getConfiguration().getOrigin()
										.getIdentification()) }));

		if (other != null)
			return other;

		return null;
	}

	/**
	 * 
	 * @param element
	 * @return
	 */
	private boolean identify(Element element) {
		if (element.getToTimeStamp() != null)
			return false;

		Integer count = identificationService.identified(element);
		if (count == 0)
			return false;

		if (count == 1)
			return true;

		if (count > 1)
			logger.error(
					"Identification found {} instanses of element [name: {}, type: {}]",
					new Object[] { count, element.getName(),
							element.getType().getName() });

		return true;
	}
}
